package org.springframework.boot.web.reactive.context;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.context.ConfigurableWebServerApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.metrics.StartupStep;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.StringUtils;

/**
 * A {@link GenericReactiveWebApplicationContext} that can be used to bootstrap itself
 * from a contained {@link ReactiveWebServerFactory} bean.
 *
 * @author Brian Clozel
 * @since 2.0.0
 */
public class ReactiveWebServerApplicationContext extends GenericReactiveWebApplicationContext
        implements ConfigurableWebServerApplicationContext {

    private volatile WebServerManager serverManager;

    private String serverNamespace;

    /**
     * Create a new {@link ReactiveWebServerApplicationContext}.
     */
    public ReactiveWebServerApplicationContext() {

    }

    /**
     * Create a new {@link ReactiveWebServerApplicationContext} with the given
     * {@code DefaultListableBeanFactory}.
     * @param beanFactory the DefaultListableBeanFactory instance to use for this context
     */
    public ReactiveWebServerApplicationContext(DefaultListableBeanFactory beanFactory) {
        super(beanFactory);
    }

    @Override
    public void refresh() throws BeansException, IllegalStateException {
        try {
            super.refresh();
        } catch (RuntimeException ex) {
            WebServerManager serverManager = this.serverManager;
            if (serverManager != null) {
                serverManager.getWebServer().stop();
            }
            throw ex;
        }
    }

    @Override
    protected void onRefresh() throws BeansException {
        super.onRefresh();
        try {
            createWebServer();
        } catch (Throwable ex) {
            throw new ApplicationContextException("Unable to start reactive web server", ex);
        }
    }

    private void createWebServer() {
        WebServerManager serverManager = this.serverManager;
        if (serverManager == null) {
            StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
            String webServerFactoryBeanName = getWebServerFactoryBeanName();
            ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName);
            createWebServer.tag("factory",webServerFactory.getClass().toString());
            boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
            this.serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit);
            getBeanFactory().registerSingleton("webServerGracefulShutdown",
                    new WebServerGracefulShutdownLifecycle(this.serverManager));
            getBeanFactory().registerSingleton("webServerStartStop",
                    new WebServerStartStopLifecycle(this.serverManager));
            createWebServer.end();
        }
        initPropertySources();
    }

    protected String getWebServerFactoryBeanName() {
        // Use bean names so that we don't consider the hierarchy
        String[] beanNames = getBeanFactory().getBeanNamesForType(ReactiveWebServerFactory.class);
        if (beanNames.length == 0) {
            throw new ApplicationContextException(
                    "Unable to start ReactiveWebApplicationContext due to missing ReactiveWebServerFactory bean.");
        }
        if (beanNames.length > 1) {
            throw new ApplicationContextException("Unable to start ReactiveWebApplicationContext due to multiple "
                    + "ReactiveWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
        }
        return beanNames[0];
    }

    protected ReactiveWebServerFactory getWebServerFactory(String factoryBeanName) {
        return getBeanFactory().getBean(factoryBeanName,ReactiveWebServerFactory.class);
    }

    /**
     * Return the {@link HttpHandler} that should be used to process the reactive web
     * server. By default this method searches for a suitable bean in the context itself.
     * @return a {@link HttpHandler} (never {@code null}
     */
    protected HttpHandler getHttpHandler() {
        // Use bean names so that we don't consider the hierarchy
        String[] beanNames = getBeanFactory().getBeanNamesForType(HttpHandler.class);
        if (beanNames.length == 0) {
            throw new ApplicationContextException(
                    "Unable to start ReactiveWebApplicationContext due to missing HttpHandler bean.");
        }
        if (beanNames.length > 1) {
            throw new ApplicationContextException(
                    "Unable to start ReactiveWebApplicationContext due to multiple HttpHandler beans : "
                            + StringUtils.arrayToCommaDelimitedString(beanNames));
        }
        return getBeanFactory().getBean(beanNames[0],HttpHandler.class);
    }

    @Override
    protected void doClose() {
        if (isActive()) {
            AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC);
        }
        super.doClose();
    }

    /**
     * Returns the {@link WebServer} that was created by the context or {@code null} if
     * the server has not yet been created.
     * @return the web server
     */
    @Override
    public WebServer getWebServer() {
        WebServerManager serverManager = this.serverManager;
        return serverManager != null ? serverManager.getWebServer() : null;
    }

    @Override
    public String getServerNamespace() {
        return serverNamespace;
    }

    @Override
    public void setServerNamespace(String serverNamespace) {
        this.serverNamespace = serverNamespace;
    }
}
